/* Copyright (c) 2007-2008, Jeffrey R Griffin
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of Jeffrey R Griffin nor the
*       names of his contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY Jeffrey R Griffin ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL <copyright holder> BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package net.visibleblue.android.os;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.net.URISyntaxException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import android.content.ComponentName;
import android.content.Intent;
import android.net.ContentURI;
import android.os.Bundle;

public final class PersistentParcel {

	private static final int PARCEL_TYPE_NULL = Byte.MAX_VALUE;
	private static final int PARCEL_TYPE_STRING = 0;
	private static final int PARCEL_TYPE_INT = 1;
	private static final int PARCEL_TYPE_ARRAY = 15;
	private static final int PERSISTABLE_TYPE_PERSISTABLE = Byte.MAX_VALUE-1;
	private static final int PERSISTABLE_TYPE_INTENT = Byte.MAX_VALUE-2;
	private static final int PERSISTABLE_TYPE_CONTENT = Byte.MAX_VALUE-3;
	private static final int PERSISTABLE_TYPE_COMPONENT = Byte.MAX_VALUE-4;
	private static final int PERSISTABLE_TYPE_BUNDLE = Byte.MAX_VALUE-5;
	private static final int ARRAY_TYPE_OBJECT = Byte.MAX_VALUE-6;
	private static final CharSequence BEGIN_CDATA = "<![CDATA[";
	private static final CharSequence END_CDATA = "]]>";
	private static final Charset CHARSET = Charset.forName("ISO-8859-1");
	private static final CharsetEncoder ENCODER = CHARSET.newEncoder();
	private static final CharsetDecoder DECODER = CHARSET.newDecoder();
	private final HashMap<String, String> ALIAS_MAP = new HashMap<String, String>();
	private static final char ESCAPE_CHAR = '\\';
	private static final char TERM_CHAR = ':';



	private final CharBuffer _charBuffer;
	private final ByteBuffer _byteBuffer;
	private final ClassLoader _loader;
	private final FileChannel _channel;
	private final ReadableByteChannel _readChannel;

	public PersistentParcel(ClassLoader loader, InputStream in)
	{
		_loader = loader;
		_readChannel = Channels.newChannel(in);
		_byteBuffer = ByteBuffer.allocate(128);
		fillBuffer();
		_channel = null;
		_charBuffer = null;
	}

	public PersistentParcel(ClassLoader loader, File file) throws FileNotFoundException
	{
		_loader = loader;
		_channel = new RandomAccessFile(file, "rw").getChannel();
		_byteBuffer = ByteBuffer.allocate(128);
		fillBuffer();
		_readChannel = null;
		_charBuffer = null;
	}

	public PersistentParcel(ClassLoader loader, CharSequence dataChars)
	{
		_loader = loader;
		_charBuffer = CharBuffer.wrap(dataChars);
		_readChannel = null;
		_byteBuffer = null;
		_channel = null;
	}

	public void addClassAlias(String alias, Class clazz)
	{
		ALIAS_MAP.put(alias, clazz.getName());
	}

	public Persistable readPersistable()
	{
		CharSequence first = readChars();
		if(first==null)
			return null;
		String name = first.toString();
		if(ALIAS_MAP.containsKey(name))
			name = ALIAS_MAP.get(name);
		Class clazz = null;
		try{clazz=_loader.loadClass(name);}
		catch(ClassNotFoundException cnfEx)
		{
			throw new RuntimeException(cnfEx);
		}
		Field[] fields = clazz.getFields();
		for(int i=0; i<fields.length; i++)
		{
			Class fieldType = fields[i].getType();
			if(Persistable.Creator.class.isAssignableFrom(fieldType))
			{
				fields[i].setAccessible(true);
				Persistable.Creator creator = null;
				try{creator = (Persistable.Creator)fields[i].get(null);}
				catch(IllegalAccessException illEx){}
				return creator.createFromParcel(this);
			}
		}
		return null;
	}

	public void writePersistable(Persistable persistable) throws IOException
	{
		if(persistable==null)
		{
			writeInt(PARCEL_TYPE_NULL);
			return;
		}
		String className = persistable.getClass().getName();
		while(ALIAS_MAP.entrySet().iterator().hasNext())
		{
			Map.Entry<String, String> entry = ALIAS_MAP.entrySet().iterator().next();
			if(entry.getValue().equals(className))
			{
				className = entry.getKey();
				break;
			}
		}
		writeChars(className);
		persistable.writeToParcel(this);
	}

	public Intent readIntent()
	{
		CharSequence first = readChars();
		if(first==null)
			return null;
		Intent intent = new Intent();
		CharSequence action = first;
		if(action.length()>0)
			intent.setAction(action.toString());
		ContentURI data = readContentURI();
		if(data!=null)
			intent.setData(data);
		ComponentName component = readComponentName();
		if(component!=null)
			intent.setComponent(component);
		Bundle extras = readBundle();
		if(extras!=null)
			intent.putExtras(extras);
		return intent;
	}

	public void writeIntent(Intent intent) throws IOException
	{
		if(intent==null)
		{
			writeInt(PARCEL_TYPE_NULL);
			return;
		}
		writeChars(intent.getAction());
		writeContentURI(intent.getData());
		writeComponentName(intent.getComponent());
		writeBundle(intent.getExtras());
	}

	public ContentURI readContentURI()
	{
		CharSequence first = readChars();
		if(first==null)
			return null;
		CharSequence content = first;
		if(content.length()==0)
			return null;
		try	{return new ContentURI(content.toString());}
		catch(URISyntaxException uriEx)
		{
			throw new RuntimeException(uriEx);
		}
	}

	public void writeContentURI(ContentURI content) throws IOException
	{
		if(content==null)
		{
			writeChars("");
			return;
		}
		writeChars(content.toString());
	}

	public ComponentName readComponentName()
	{
		CharSequence first = readChars();
		if(first==null)
			return null;
		CharSequence component = first;
		if(component.length()==0)
			return null;
		return ComponentName.unflattenFromString(component.toString());
	}

	public void writeComponentName(ComponentName component) throws IOException
	{
		if(component==null)
		{
			writeChars("");
			return;
		}
		writeChars(component.flattenToString());
	}

	public Bundle readBundle()
	{
		int first = readInt();
		if(first==PARCEL_TYPE_NULL)
			return null;
		int size = first;
		Bundle bundle = new Bundle(size);
		Map<String, Object> map = bundle.getAsMap();
		for(int i=0; i<size; i++)
		{
			CharSequence key = readChars();
			Object value = readObject();
			map.put(key.toString(), value);
		}
		return bundle;
	}

	public void writeBundle(Bundle bundle) throws IOException
	{
		if(bundle==null)
		{
			writeInt(PARCEL_TYPE_NULL);
			return;
		}
		writeInt(bundle.size());
		Map<String, Object> asMap = bundle.getAsMap();
		Iterator<Map.Entry<String, Object>> iterator = asMap.entrySet().iterator();
		while(iterator.hasNext())
		{
			Map.Entry<String, Object> entry = iterator.next();
			writeChars(entry.getKey());
			writeObject(entry.getValue());
		}
	}

	public Object readArray()
	{
		int first = readInt();
		if(first==PARCEL_TYPE_NULL)
			return null;
		int size = first;
		
		Object array = Array.newInstance(getArrayClass(), size);
		for(int i=0; i<Array.getLength(array); i++)
			Array.set(array, i, readObject());
		return array;
	}
	
	private Class getArrayClass()
	{
		int type = readInt();
		switch(type)
		{
		case PARCEL_TYPE_NULL:
			return null;
		case PARCEL_TYPE_INT:
			return int.class;
		case PARCEL_TYPE_STRING:
			return String.class;
		case PERSISTABLE_TYPE_BUNDLE:
			return Bundle.class;
		case PERSISTABLE_TYPE_COMPONENT:
			return ComponentName.class;
		case PERSISTABLE_TYPE_CONTENT:
			return ContentURI.class;
		case PERSISTABLE_TYPE_INTENT:
			return Intent.class;
		case PERSISTABLE_TYPE_PERSISTABLE:
			return Persistable.class;
		case ARRAY_TYPE_OBJECT:
			return Object.class;
		case PARCEL_TYPE_ARRAY:
			return Array.newInstance(getArrayClass(), 0).getClass();
		default:
			throw new RuntimeException("Unknown type constant.");
		}
	}

	public void writeArray(Object array) throws IOException
	{
		if(array==null)
		{
			writeInt(PARCEL_TYPE_NULL);
			return;
		}
		int size = Array.getLength(array);
		writeInt(size);
		writeArrayType(array.getClass().getComponentType());
		for(int i=0; i<size; i++)
		{
			writeObject(Array.get(array, i));
		}

	}
	
	private void writeArrayType(Class componentType) throws IOException
	{
		if (componentType.equals(Integer.class) || 
				componentType.equals(int.class)) {
			writeInt(PARCEL_TYPE_INT);
		}
		else if (componentType.equals(String.class))
			writeInt(PARCEL_TYPE_STRING);
		else if (componentType.equals(Bundle.class))
			writeInt(PERSISTABLE_TYPE_BUNDLE);
		else if (componentType.equals(ComponentName.class))
			writeInt(PERSISTABLE_TYPE_COMPONENT);
		else if (componentType.equals(ContentURI.class))
			writeInt(PERSISTABLE_TYPE_CONTENT);
		else if (componentType.equals(Intent.class))
			writeInt(PERSISTABLE_TYPE_INTENT);
		else if (componentType.equals(Persistable.class))
			writeInt(PERSISTABLE_TYPE_PERSISTABLE);
		else if (componentType.equals(Object.class))
			writeInt(ARRAY_TYPE_OBJECT);
		else if (componentType.isArray())
		{
			writeInt(PARCEL_TYPE_ARRAY);
			writeArrayType(componentType.getComponentType());
		}
		else
			throw new RuntimeException("Unable to convert object type.");
	}

	//Some objects can store any type.  Parcels use a type constant to distinguish them.
	public Object readObject()
	{
		int type = readInt();
		switch(type)
		{
		case PARCEL_TYPE_NULL:
			return null;
		case PARCEL_TYPE_INT:
			return readInt();
		case PARCEL_TYPE_STRING:
			return readChars().toString();
		case PERSISTABLE_TYPE_BUNDLE:
			return readBundle();
		case PERSISTABLE_TYPE_COMPONENT:
			return readComponentName();
		case PERSISTABLE_TYPE_CONTENT:
			return readContentURI();
		case PERSISTABLE_TYPE_INTENT:
			return readIntent();
		case PERSISTABLE_TYPE_PERSISTABLE:
			return readPersistable();
		case PARCEL_TYPE_ARRAY:
			return readArray();
		default:
			throw new RuntimeException("Unknown type constant.");
		}
	}

	public void writeObject(Object obj) throws IOException
	{
		if(obj==null)
			writeInt(PARCEL_TYPE_NULL);
		else if (obj instanceof Integer) {
			Integer intVal = (Integer)obj;
			writeInt(PARCEL_TYPE_INT);
			writeInt(intVal);
		}
		else if (obj instanceof String) {
			String stringVal = (String) obj;
			writeInt(PARCEL_TYPE_STRING);
			writeChars(stringVal);
		}
		else if (obj instanceof Bundle) {
			Bundle bundleVal = (Bundle) obj;
			writeInt(PERSISTABLE_TYPE_BUNDLE);
			writeBundle(bundleVal);
		}
		else if (obj instanceof ComponentName) {
			ComponentName componentVal = (ComponentName) obj;
			writeInt(PERSISTABLE_TYPE_COMPONENT);
			writeComponentName(componentVal);
		}
		else if (obj instanceof ContentURI) {
			ContentURI contentVal = (ContentURI) obj;
			writeInt(PERSISTABLE_TYPE_CONTENT);
			writeContentURI(contentVal);
		}
		else if (obj instanceof Intent) {
			Intent intentVal = (Intent) obj;
			writeInt(PERSISTABLE_TYPE_INTENT);
			writeIntent(intentVal);
		}
		else if (obj instanceof Persistable) {
			Persistable persistVal = (Persistable) obj;
			writeInt(PERSISTABLE_TYPE_PERSISTABLE);
			writePersistable(persistVal);
		}
		else if (obj.getClass().isArray()) {
			writeInt(PARCEL_TYPE_ARRAY);
			writeArray(obj);
		}
		else
			throw new RuntimeException("Unable to convert object type.");
	}

	public int readInt()
	{
		StringBuilder builder = new StringBuilder();
		char val;
		while((val=get())!=TERM_CHAR)
		{
			builder.append(val);
			if(builder.length()>8) //Integer.toHexString(Integer.MAX_VALUE).length()
				throw new RuntimeException("Cannot resolve value to int.");
		}
		//return builder;
		return Integer.parseInt(builder.toString(), 16);
	}

	public void writeInt(int intVal) throws IOException
	{
		writeChars(Integer.toHexString(intVal));
	}

	private CharBuffer _asChars;
	private char get()
	{
		if(_charBuffer!=null)
			_asChars = _charBuffer;
		if(_asChars==null)
		{
			try
			{
				_asChars = DECODER.decode(_byteBuffer);
			}
			catch(CharacterCodingException ccEx)
			{
				throw new RuntimeException(ccEx);
			}
		}
		if(_asChars.hasRemaining())
			return _asChars.get();
		else if(_channel!=null || _readChannel!=null)
		{
			_asChars = null;
			fillBuffer();
			return get();
		}
		else
			throw new BufferUnderflowException();
	}

	private void fillBuffer()
	{	
		_byteBuffer.clear();
		try
		{
			int size = 0;
			if(_channel!=null && _channel.size()>0)
				size = _channel.read(_byteBuffer);
			else if(_readChannel!=null)
				size = _readChannel.read(_byteBuffer);
			if(size==-1)
				throw new BufferUnderflowException();
		}
		catch(IOException ioEx)
		{
			throw new RuntimeException(ioEx);
		}
		_byteBuffer.flip();
	}

	public CharSequence readChars()
	{
		StringBuilder builder = new StringBuilder();
		while(true)
		{
			char val = get();
			if(val==ESCAPE_CHAR)
				builder.append(get());
			else if(val==TERM_CHAR)
				break;
			else
				builder.append(val);

			if(builder.length()==9 && builder.toString().equals(BEGIN_CDATA))
				return readToCDATATerminator(builder);
		}
		if(builder.length()==1 && builder.charAt(0)==PARCEL_TYPE_NULL)
			return null;
		return builder;
	}

	public void writeChars(CharSequence chars) throws IOException
	{
		if(_channel==null)
		{
			throw new RuntimeException("Persistent Parcel must be instantiated with a File "+
			"in order to be writable");
		}
		StringBuilder builder = new StringBuilder(chars);
		for(int i=Math.max(builder.lastIndexOf(Character.toString(TERM_CHAR), builder.length()), 
				builder.lastIndexOf(Character.toString(ESCAPE_CHAR), builder.length()));
		i!=-1;
		i=Math.max(builder.lastIndexOf(Character.toString(TERM_CHAR), i-1), 
				builder.lastIndexOf(Character.toString(ESCAPE_CHAR), i-1)))
		{
			builder.insert(i, ESCAPE_CHAR);
		}
		builder.append(TERM_CHAR);
		writeCharsInternal(builder);
	}

	private void writeCDATA(StringBuilder builder) throws IOException
	{
		builder.insert(0, BEGIN_CDATA);
		builder.append(END_CDATA);
		writeCharsInternal(builder);
	}

	private void writeCharsInternal(StringBuilder builder) throws IOException
	{
		if(_byteBuffer.position()!=0)
		{
			_channel.position(_channel.position()-_byteBuffer.position());
			_byteBuffer.clear();
		}
		_channel.write(ENCODER.encode(CharBuffer.wrap(builder)));
	}

	private CharSequence readToCDATATerminator(StringBuilder builder)
	{
		builder.delete(0, builder.length());
		while(true)
		{
			builder.append(get());
			if(builder.length()>=3)
			{
				int index = builder.indexOf(END_CDATA.toString(), builder.length()-3);
				if(index!=-1)
					return builder.subSequence(0, index);
			}
		}
	}

}
